package com.eshipenet.shipnet.equipmentdatahub

import java.util.concurrent.TimeUnit

import org.joda.time.{DateTime, Period}

import scala.concurrent.duration.FiniteDuration

case class EquipmentDataEntry(elem: Option[EdgeChannelData], prevTimestampNS: Option[Long])

class EquipmentDataState(val equipment: EquipmentWithModel,
                         private val dataMap: Map[String, EquipmentDataEntry],
                         private val timestampNS: Option[Long] = None,
                         val shouldDelay: Option[FiniteDuration] = None) {

  def isUpdated(newEquipment: EquipmentWithModel): Boolean = {
    if (equipment.model.period.getMillis != newEquipment.model.period.getMillis) {
      return true
    }
    false
  }

  def resetWith(newEquipment: EquipmentWithModel): EquipmentDataState = {
    new EquipmentDataState(newEquipment, dataMap)
  }

//  def isSubscribing(data: EdgeChannelData): Boolean = {
//
//  }

  def input(data: EdgeChannelData): EquipmentDataState = {
    if (data.id == "EdgeBox.EB00000001.edgechannel.iimwv.direction") {
      println(s"input data: $data")
    }
    val t    = timestampNS.getOrElse(data.timestampNS)
    val diff = math.abs(data.timestampNS - t)
    if (diff <= equipment.model.period.getMillis.toLong * 1000 * 1000 / 2) {
      // a data in slot
      if (data.id == "EdgeBox.EB00000001.edgechannel.iimwv.direction") {
        println("input: a data in slot")
      }
      val newEntry = dataMap.get(data.id) match {
        case Some(entry) =>
          entry match {
            case EquipmentDataEntry(Some(elem), Some(nt)) =>
              // select the more close one
              val e = selectClose(elem, data, nt)
              EquipmentDataEntry(Some(e), Some(nt))
            case EquipmentDataEntry(_, nt) =>
              EquipmentDataEntry(Some(data), nt)
          }
        case _ =>
          EquipmentDataEntry(Some(data), None)
      }
      new EquipmentDataState(equipment, dataMap.updated(data.id, newEntry), Some(t))
    } else if (data.timestampNS > t) {
      // data from future
      val m = data.timestampNS - t
      if (data.id == "EdgeBox.EB00000001.edgechannel.iimwv.direction") {
        println(s"input: data from future: tm = ${new DateTime(data.timestampNS / 1000 / 1000)} diff = $diff m = $m")
      }
      new EquipmentDataState(equipment, dataMap, Some(t), Some(FiniteDuration(m, TimeUnit.NANOSECONDS)))
    } else {
      if (data.id == "EdgeBox.EB00000001.edgechannel.iimwv.direction") {
        println(s"input: data out of slot: $diff")
      }
      new EquipmentDataState(equipment, dataMap, Some(t))
    }
  }

  private def selectClose(data1: EdgeChannelData, data2: EdgeChannelData, timestampNS: Long): EdgeChannelData = {
    val diff1 = math.abs(data1.timestampNS - timestampNS)
    val diff2 = math.abs(data2.timestampNS - timestampNS)
    if (diff1 < diff2) {
      data1
    } else {
      data2
    }
  }

//  def input(data: EdgeChannelData): EquipmentDataState = this

  def output(): (EquipmentDataState, EquipmentData) = {
    val newDataMap = dataMap.mapValues { entry =>
      if (entry.elem.isDefined) {
        val d = entry.elem.get
        val nd = if (isStale(d, timestampNS, equipment.model.period)) {
          None
        } else {
          Some(d)
        }
        (EquipmentDataEntry(nd, Some(entry.elem.get.timestampNS)), Some(d))
      } else {
        (entry, None)
      }
    }
    val ndm          = newDataMap.mapValues(_._1)
    val data         = newDataMap.mapValues(_._2).filter(_._2.isDefined).mapValues(_.get)
    val mappedData   = mappingNames(data, equipment.model.instData)
    val newTimestamp = timestampNS.map(t => t + equipment.model.period.getMillis * 1000 * 1000)
    val timestamp    = newTimestamp.getOrElse(meanTimestamp(data))
    (new EquipmentDataState(equipment, ndm, newTimestamp),
     EquipmentData(equipment = equipment.equipment, model = equipment.model, data = mappedData, timestamp))
  }

  private def meanTimestamp(data: Map[String, EdgeChannelData]): Long = {
    (data.map(_._2.timestampNS).map(BigDecimal(_)).sum / data.size).toLong
  }

  private def mappingNames(data: Map[String, EdgeChannelData],
                           instData: Map[String, EquipmentModelDatumSchema]): Map[String, EdgeChannelData] = {
    data map { kv =>
      val id    = kv._1
      val value = kv._2
      val name  = equipment.idToName(id)
      (name, value)
    }
  }

  private def isStale(data: EdgeChannelData, maybeTime: Option[Long], span: Period): Boolean = {
    maybeTime match {
      case Some(t) =>
        val diff = math.abs(t - data.timestampNS)
        diff > span.getMillis * 1000 * 1000
      case _ => false
    }
  }
}
